/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.google.devtools.j2objc.ast;

import com.google.devtools.j2objc.GenerationTest;
import java.io.IOException;

/**
 * Unit tests for {@link MethodReference}.
 *
 * @author Seth Kirby
 */
public class MethodReferenceTest extends GenerationTest {

  // Test the creation of explicit blocks for lambdas with expression bodies.
  public void testCreationReferenceBlockWrapper() throws IOException {
    String creationReferenceHeader =
        """
        class I {
          I() {}
          I(int x) {}
          I(int x, I j, String s, Object o) {}
        }
        interface FunInt<T> {
          T apply(int x);
        }
        interface FunInt4<T> {
          T apply(int x, I j, String s, Object o);
        }
        interface Call<T> {
          T call();
        }
        """;

    String noArgumentTranslation = translateSourceFile(
        creationReferenceHeader + "class Test { Call<I> iInit = I::new; }",
        "Test", "Test.m");
    assertTranslatedLines(noArgumentTranslation,
        "- (id)call {",
        "  return create_I_init();",
        "}");
    String oneArgumentTranslation = translateSourceFile(
        creationReferenceHeader + "class Test { FunInt<I> iInit2 = I::new; }", "Test", "Test.m");
    assertTranslatedLines(oneArgumentTranslation,
        "- (id)applyWithInt:(int32_t)a {",
        "  return create_I_initWithInt_(a);",
        "}");
    String mixedArgumentTranslation = translateSourceFile(
        creationReferenceHeader + "class Test { FunInt4<I> iInit3 = I::new; }", "Test", "Test.m");
    assertTranslatedLines(mixedArgumentTranslation,
        "- (id)applyWithInt:(int32_t)a",
        "             withI:(I *)b",
        "      withNSString:(NSString *)c",
        "            withId:(id)d {",
        "  return create_I_initWithInt_withI_withNSString_withId_(a, b, c, d);",
        "}");
  }

  // Test that expression method references resolve correctly for static and non-static methods.
  public void testExpressionReferenceStaticResolution() throws IOException {
    String expressionReferenceHeader =
        """
        class Q {
          static Object o(Object x) {
            return x;
          }
          Object o2(Object x) {
            return x;
          }
        }
        interface F<T, R> {
          R f(T t);
        }
        """;
    String staticTranslation = translateSourceFile(
        expressionReferenceHeader + "class Test { F fun = Q::o; }",
        "Test", "Test.m");
    // Should be non-capturing.
    assertInTranslation(
        staticTranslation,
        "JreStrongAssign(&self->fun_, JreLoadStatic(Test_$Lambda$1, instance));");
    assertTranslatedLines(staticTranslation,
        "- (id)fWithId:(id)a {",
        "  return Q_oWithId_(a);",
        "}");
    String instanceTranslation = translateSourceFile(
        expressionReferenceHeader + "class Test { F fun = new Q()::o2; }",
        "Test", "Test.m");
    // Should be capturing.
    assertInTranslation(
        instanceTranslation,
        "JreStrongAssignAndConsume(&self->fun_, new_Test_$Lambda$1_initWithQ_(create_Q_init()));");
    assertTranslatedLines(instanceTranslation,
        "- (id)fWithId:(id)a {",
        "  return JreRetainedLocalValue([target$_ o2WithId:a]);",
        "}");
    String staticInstanceTranslation = translateSourceFile(
        expressionReferenceHeader + "class Test { static F fun = new Q()::o2; }",
        "Test", "Test.m");
    assertInTranslation(
        staticInstanceTranslation,
        "JreStrongAssignAndConsume(&Test_fun, new_Test_$Lambda$1_initWithQ_(create_Q_init()));");
    assertTranslatedLines(staticInstanceTranslation,
        "- (id)fWithId:(id)a {",
        "  return JreRetainedLocalValue([target$_ o2WithId:a]);",
        "}");
  }

  public void testTypeReference() throws IOException {
    String typeReferenceHeader = "interface H { Object copy(int[] i); }";
    String translation = translateSourceFile(
        typeReferenceHeader + "class Test { H h = int[]::clone; }", "Test", "Test.m");
    assertTranslatedLines(translation,
        "- (id)copy__WithIntArray:(IOSIntArray *)a {",
        "  return JreRetainedLocalValue([((IOSIntArray *) nil_chk(a)) java_clone]);",
        "}");
  }

  public void testReferenceToInstanceMethodOfType() throws IOException {
    String source =
        """
        import java.util.Comparator;
        class Test {
          void f() {
            Comparator<String> s = String::compareTo;
          }
        }
        """;

    String impl = translateSourceFile(source, "Test", "Test.m");
    assertInTranslation(impl, "return [((NSString *) nil_chk(a)) compareToWithId:b];");
  }

  public void testReferenceToInstanceMethodOfGenericType() throws IOException {
    String source =
        """
        interface BiConsumer<T, U> {
          void accept(T t, U u);
        }
        interface Collection<E> {
          boolean add(E x);
        }
        class Test {
          <T> void f(Collection<T> c, T o) {
            BiConsumer<Collection<T>, T> bc = Collection<T>::add;
          }
        }
        """;

    String impl = translateSourceFile(source, "Test", "Test.m");
    assertInTranslation(impl, "[((id<Collection>) nil_chk(a)) addWithId:b];");
    assertNotInTranslation(impl, "return [((id<Collection>) nil_chk(a)) addWithId:b];");
  }

  public void testReferenceToInstanceMethodOfGenericTypeWithReturnType() throws IOException {
    String source =
        """
        interface BiConsumer<T, U> {
          boolean accept(T t, U u);
        }
        interface Collection<E> {
          boolean add(E x);
        }
        class Test {
          <T> void f(Collection<T> c, T o) {
            BiConsumer<Collection<T>, T> bc = Collection<T>::add;
          }
        }
        """;

    String impl = translateSourceFile(source, "Test", "Test.m");
    assertInTranslation(impl, "return [((id<Collection>) nil_chk(a)) addWithId:b];");
  }

  public void testVarArgs() throws IOException {
    String varArgsHeader =
        """
        interface I {
          void foo(int a1, String a2, String a3);
        }
        interface I2 {
          void foo(int a1, String a2, String a3, String a4);
        }
        class Y {
          static void m(int a1, String... rest) {}
        }
        """;
    String translation =
        translateSourceFile(
            varArgsHeader
                + """
                class Test {
                  I i = Y::m;
                  I2 i2 = Y::m;
                }
                """,
            "Test",
            "Test.m");
    assertTranslatedLines(translation,
        "- (void)fooWithInt:(int32_t)a",
        "      withNSString:(NSString *)b",
        "      withNSString:(NSString *)c {",
        "  Y_mWithInt_withNSStringArray_(a, [IOSObjectArray arrayWithObjects:(id[]){ b, c } "
            + "count:2 type:NSString_class_()]);",
        "}");
    assertTranslatedLines(translation,
        "- (void)fooWithInt:(int32_t)a",
        "      withNSString:(NSString *)b",
        "      withNSString:(NSString *)c",
        "      withNSString:(NSString *)d {",
        "  Y_mWithInt_withNSStringArray_(a, [IOSObjectArray arrayWithObjects:(id[]){ b, c, d } "
            + "count:3 type:NSString_class_()]);",
        "}");
  }

  public void testReferenceToInstanceMethodOfTypeWithVarArgs() throws IOException {
    String p = "interface P<T> { void f(T t); }";
    String q = "interface Q<T> { void f(T t, String a, String b); }";
    String r = "interface R<T> { void f(T t, String... rest); }";
    String x1 =
        p
            + """
            class X {
              void g(String... rest) {}
              void h() {
                P<X> ff = X::g;
              }
            }
            """;
    String x2 =
        q
            + """
            class X {
              void g(String... rest) {}
              void h() {
                Q<X> ff = X::g;
              }
            }
            """;
    String x3 =
        r
            + """
            class X {
              void g(String... rest) {}
              void h() {
                R<X> ff = X::g;
              }
            }
            """;
    String impl1 = translateSourceFile(x1, "X", "X.m");
    String impl2 = translateSourceFile(x2, "X", "X.m");
    String impl3 = translateSourceFile(x3, "X", "X.m");

    // Pass an empty array to the referenced method.
    assertTranslatedLines(impl1,
        "- (void)fWithId:(X *)a {",
        "  [((X *) nil_chk(a)) gWithNSStringArray:"
            + "[IOSObjectArray arrayWithLength:0 type:NSString_class_()]];",
        "}");

    // Pass an array of the arguments b and c to the referenced method.
    assertTranslatedLines(impl2,
        "- (void)fWithId:(X *)a",
        "   withNSString:(NSString *)b",
        "   withNSString:(NSString *)c {",
        "  [((X *) nil_chk(a)) gWithNSStringArray:[IOSObjectArray arrayWithObjects:(id[]){ b, c } "
            + "count:2 type:NSString_class_()]];",
        "}");

    // Pass the varargs array to the referenced method.
    assertTranslatedLines(impl3,
        "- (void)fWithId:(X *)a",
        "withNSStringArray:(IOSObjectArray *)b {",
        "  [((X *) nil_chk(a)) gWithNSStringArray:b];",
        "}");
  }

  public void testArgumentBoxingAndUnboxing() throws IOException {
    String header =
        """
        interface IntFun {
          void apply(int a);
        }
        interface IntegerFun {
          void apply(Integer a);
        }
        """;
    String translation =
        translateSourceFile(
            header
                + """
                class Test {
                  static void foo(Integer x) {}
                  static void bar(int x) {}
                  IntFun f = Test::foo;
                  IntegerFun f2 = Test::bar;
                }
                """,
            "Test",
            "Test.m");
    assertTranslatedLines(translation,
        "- (void)applyWithInt:(int32_t)a {",
        "  Test_fooWithJavaLangInteger_(JavaLangInteger_valueOfWithInt_(a));",
        "}");
    assertTranslatedLines(translation,
        "- (void)applyWithJavaLangInteger:(JavaLangInteger *)a {",
        "  Test_barWithInt_([((JavaLangInteger *) nil_chk(a)) intValue]);",
        "}");
  }

  public void testReturnBoxingAndUnboxing() throws IOException {
    String header =
        """
        interface Fun {
          Integer a();
        }
        interface Fun2 {
          int a();
        }
        """;
    String translation =
        translateSourceFile(
            header
                + """
                class Test {
                  int size() {
                    return 42;
                  }
                  Integer size2() {
                    return 43;
                  }
                  Fun f = this::size;
                  Fun2 f2 = this::size2;
                }
                """,
            "Test",
            "Test.m");
    assertTranslatedLines(translation,
        "- (JavaLangInteger *)a {",
        "  return JavaLangInteger_valueOfWithInt_([target$_ size]);",
        "}");
    assertTranslatedLines(translation,
        "- (int32_t)a {",
        "  return [((JavaLangInteger *) nil_chk([target$_ size2])) intValue];",
        "}");
  }

  // Creation references can be initialized only for side effects, and have a void return.
  public void testCreationReferenceVoidReturn() throws IOException {
    String header = "interface V { void f(); }";
    String translation =
        translateSourceFile(
            header
                + """
                class Test {
                  V v = Test::new;
                }
                """,
            "Test",
            "Test.m");
    assertTranslatedLines(translation,
        "- (void)f {",
        "  create_Test_init();",
        "}");
  }

  public void testCreationReferenceNonVoidReturn() throws IOException {
    String header = "interface V { Object f(); }";
    String translation =
        translateSourceFile(
            header
                + """
                class Test {
                  V v = Test::new;
                }
                """,
            "Test",
            "Test.m");
    assertTranslatedLines(translation, "- (id)f {", "return create_Test_init();");
  }

  public void testArrayCreationReference() throws IOException {
    String translation =
        translateSourceFile(
            """
            import java.util.function.Supplier;
            interface IntFunction<R> {
              R apply(int value);
            }
            class Test {
              IntFunction<int[]> i = int[]::new;
            }
            """,
            "Test",
            "Test.m");
    assertNotInTranslation(translation, "return create_IntFunction_initWithIntArray_");
    assertTranslatedLines(translation,
        "- (id)applyWithInt:(int32_t)a {",
        "  return JreRetainedLocalValue([IOSIntArray arrayWithLength:a]);",
        "}");
  }

  public void testCreationReferenceOfLocalCapturingType() throws IOException {
    String translation =
        translateSourceFile(
            """
            interface Supplier<T> {
              T get();
            }
            class Test {
              static Supplier<Runnable> test(Runnable r) {
                class Runner implements Runnable {
                  public void run() {
                    r.run();
                  }
                }
                return Runner::new;
              }
            }
            """,
            "Test",
            "Test.m");
    assertTranslatedLines(translation,
        "void Test_$Lambda$1_initWithJavaLangRunnable_("
            + "Test_$Lambda$1 *self, id<JavaLangRunnable> capture$0) {",
        "  JreStrongAssign(&self->val$r_, capture$0);",
        "  NSObject_init(self);",
        "}");
    assertTranslatedLines(translation,
        "- (id)get {",
        "  return create_Test_1Runner_initWithJavaLangRunnable_(val$r_);",
        "}");
  }

  public void testQualifiedSuperMethodReference() throws IOException {
    String translation =
        translateSourceFile(
            """
            interface I {
              void bar();
            }
            class Test {
              void foo() {}
              static class TestSub extends Test {
                void foo() {}
                class Inner {
                  I test() {
                    return TestSub.super::foo;
                  }
                }
              }
            }
            """,
            "Test",
            "Test.m");
    assertTranslatedLines(translation,
        "- (void)bar {",
        "  Test_foo(this$0_->this$0_);",
        "}");
  }

  public void testMultipleMethodReferencesNilChecks() throws IOException {
    String translation =
        translateSourceFile(
            """
            interface Foo {
              void f(Test t);
            }
            class Test {
              void foo() {}
              void test() {
                Foo f1 = Test::foo;
                Foo f2 = Test::foo;
              }
            }
            """,
            "Test",
            "Test.m");
    // Both lambdas must perform a nil_chk on their local variable "a".
    assertOccurrences(translation, "nil_chk(a)", 2);
  }

  public void testCapturingExpressionMethodReferences() throws IOException {
    String translation =
        translateSourceFile(
            """
            interface Supplier {
              int get();
            }
            class Holder {
              private int num;
              public Holder(int i) {
                num = i;
              }
              int get() {
                return num;
              }
            }
            class Test {
              public void run() {
                Holder h = new Holder(1);
                Supplier s = h::get;
              }
            }
            """,
            "Test",
            "Test.m");
    // Make sure there is a captured variable.
    assertTranslatedLines(translation,
        "@interface Test_$Lambda$1 : NSObject < Supplier > {",
        " @public",
        "  Holder *target$_;",
        "}");
    assertTranslatedLines(translation,
        "- (void)run {",
        "  Holder *h = create_Holder_initWithInt_(1);",
        "  id<Supplier> s = create_Test_$Lambda$1_initWithHolder_(h);",
        "}");
    // Make sure the receiver field is initialized.
    assertInTranslation(translation, "JreStrongAssign(&self->target$_, outer$);");
  }
}
